#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <X11/X.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <GL/gl.h>
#include <GL/glx.h>
#include <GL/glu.h>

uint32_t width, height;

float zoom = 0;
float offx = 0, offy = 0;

float mx, my;
int pressed = 0;

void display_image();

int main(int argc, char *argv[]) {
  size_t rowlen, pixlen;
  uint16_t *pixels, *tmp;
  uint32_t hdr[4], i;

  Display *dpy;
  Window root;
  GLint att[] = { GLX_RGBA, GLX_DOUBLEBUFFER, None };
  XVisualInfo *vi;
  Colormap cmap;
  XSetWindowAttributes swa;
  Window win;
  GLXContext glc;
  XWindowAttributes gwa;
  XEvent xev;

  GLuint texture[1];
  
  KeySym key;
  char text[255];
  float dx, dy;
  Time lasttime;
  
  // 1. Load farbfeld image into pixels buffer
  
  if (fread(hdr, sizeof(*hdr), 4, stdin) != 4) {
    fprintf(stderr, "%s: unable to read magic value\n", argv[0]);
    return 1;
  }
  if (memcmp("farbfeld", hdr, sizeof("farbfeld") - 1)) {
    fprintf(stderr, "%s: invalid magic value\n", argv[0]);
    return 1;
  }
  width = ntohl(hdr[2]);
  height = ntohl(hdr[3]);
  
  if (width > SIZE_MAX / ((sizeof("RGBA") - 1) * sizeof(uint16_t))) {
    fprintf(stderr, "%s: row length integer overflow\n", argv[0]);
    return 1;
  }
  rowlen = width * (sizeof("RGBA") - 1);
  pixlen = width * height * (sizeof("RGBA") - 1);
  if (!(pixels = malloc(pixlen * sizeof(uint16_t)))) {
    fprintf(stderr, "%s: malloc: out of memory\n", argv[0]);
    return 1;
  }
  
  tmp = pixels;
  for (i = 0; i < height; ++i) {
    if (fread(tmp, sizeof(uint16_t), rowlen, stdin) != rowlen) {
      fprintf(stderr, "%s: unable to image data, row %d\n", argv[0], i);
      return 1;
    }
    tmp += rowlen;
  }
  
  // 2. Set up X11 and OpenGL
  
  dpy = XOpenDisplay(NULL);
  if (dpy == NULL) {
    fprintf(stderr, "%s: cannot connect to X server\n", argv[0]);
    return 1;
  }
  
  root = DefaultRootWindow(dpy);
  
  vi = glXChooseVisual(dpy, 0, att);
  if (vi == NULL) {
    fprintf(stderr, "%s: no appropriate visual found\n", argv[0]);
    return 1;
  }
  
  cmap = XCreateColormap(dpy, root, vi->visual, AllocNone);
  swa.colormap = cmap;
  swa.event_mask = ExposureMask | StructureNotifyMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask;

  win = XCreateWindow(dpy, root, 0, 0, width, height, 0, vi->depth, InputOutput, vi->visual, CWColormap | CWEventMask, &swa);

  Atom wmDeleteWindow = XInternAtom(dpy, "WM_DELETE_WINDOW", True);
  XSetWMProtocols(dpy, win, &wmDeleteWindow, 1);

  XMapWindow(dpy, win);
  XStoreName(dpy, win, "ffview");

  glc = glXCreateContext(dpy, vi, NULL, GL_TRUE);
  glXMakeCurrent(dpy, win, glc);
  
  lasttime = 0;
  
  // 3. Upload image to GL texture
  
  glGenTextures(1, texture);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_RECTANGLE, texture[0]);
  glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_SHORT, pixels);
  free(pixels);
  
  glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  
  glEnable(GL_TEXTURE_RECTANGLE);

  // 4. Event loop

  while(1) {
    XNextEvent(dpy, &xev);
    
    switch(xev.type){
    case Expose:
      XGetWindowAttributes(dpy, win, &gwa);
      glViewport(0, 0, gwa.width, gwa.height);
      display_image(); 
      glXSwapBuffers(dpy, win);
      break;
    case ConfigureNotify:
      break;
    case KeyPress:
      if (xev.xkey.keycode == 0x09) {
	glXMakeCurrent(dpy, None, NULL);
	glXDestroyContext(dpy, glc);
	XDestroyWindow(dpy, win);
	XCloseDisplay(dpy);
	exit(0);
      }
      else if (XLookupString(&xev.xkey, text, 255, &key, 0) == 1) {
	switch (text[0]) {
	case '+':
	  zoom += 1;
	  break;
	case '-':
	  zoom -= 1;
	  break;
	case '=':
	  zoom = 0;
	  break;
	}
	
	display_image();
	glXSwapBuffers(dpy, win);
      }
      break;
    case KeyRelease:
      break;
    case ButtonPress:
      switch (xev.xbutton.button) {
      case Button1:
	mx = xev.xbutton.x;
	my = xev.xbutton.y;
	pressed = 1;
      default:
	break;
      }
      break;
    case ButtonRelease:
      switch (xev.xbutton.button) {
      case Button1:
	pressed = 0;

	display_image();
	glXSwapBuffers(dpy, win);
      default:
	break;
      }
    case MotionNotify:
      if (pressed) {
	dx = mx;
	mx = xev.xbutton.x;
	dx = mx-dx;
	
	dy = my;
	my = xev.xbutton.y;
	dy = my-dy;

	offx += dx * 0.001 * (1 + zoom * 0.01);
	offy += dy * 0.001 * (1 + zoom * 0.01);
	
	if ((xev.xmotion.time - lasttime) <= (1000 / 60))
	  break;
	lasttime = xev.xmotion.time;
	
	display_image();
	glXSwapBuffers(dpy, win);
      }
      break;
    case ClientMessage:
      if (xev.xclient.data.l[0] == wmDeleteWindow) {
	exit(0);
      }
      break;
    }
  }
}

void display_image() {
  glClearColor(0.0, 0.0, 0.0, 1.0);
  glClear(GL_COLOR_BUFFER_BIT);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-1, 1., 1., -1, 1., 20.);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(0., 0., 10., 0., 0., 0., 0., 1., 0.);

  glTranslatef(offx, offy, 1);
  glScalef(1 + zoom * 0.1, 1 + zoom * 0.1, 1);

  glColor3f(1., 1., 1.);
  
  glBegin(GL_QUADS);
  glTexCoord2f(0, 0);
  glVertex3f(-1, -1, 0.);
  glTexCoord2f(width, 0);
  glVertex3f(1, -1, 0.);
  glTexCoord2f(width, height);
  glVertex3f(1, 1, 0.);
  glTexCoord2f(0, height);
  glVertex3f(-1, 1, 0.);
  glEnd();
} 
